#!/usr/bin/python -u
# for testing, also use:
#!/usr/bin/python1.5 -u
#
# program: tpm - Tim's Patch Manager
#  prepare a patched version of the tree based on original sources
#  and patches
#
# Author: Tim Bird - tim.bird ``(at)~~ am.sony.com
# Copyright (C) 2003,2004 - Sony Electronics, Inc.
# License: GPL 2.0
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    See http://www.gnu.org/licenses/gpl.html for a copy of the
#    license.
#
# This is essentially my own custom patch management tool
# build phases are:
#	unpack - not done yet
# 	unarchive_source
#	apply_patches
#	copy_to_orig - make a backup copy of the source for later patch
#		creation
#	build - not done yet
#
# To Do for this program:
#

import sys, os, commands
import string

major_version = 1
minor_version = 5

default_tarfile = "base/linux-2.6.10.tar.bz2"
default_out_dir = "celinux-test"

try:
	default_patch_list_file = os.environ["PATCHLIST"]
except:
	default_patch_list_file = "../../patches/patchlist"

def endswith(line, ending):
	if line[-(len(ending)):] == ending:
		return 1
	else:
		return 0

def show_version(progpath):
	progname = os.path.basename(progpath)
	print "%s - Tim's Patch Manager, version %d.%d" % (progname, major_version, minor_version)

def usage(progpath):
	print """Usage: %s [options]
    -a <arch>		Specify architecture to build
    -b                  Build the tree after unarchiving
    -c                  Copy files to .orig directory after applying patches
    -f <patchlist>      Get the list of patches from the file <patchlist>
                         The patch search dir is set to the directory where
			 patchlist is found.
    -h, --help          Show this usage help
    -l <patch[,p2,...]> Apply the specified patches, in order
    -o <directory>	Specify output directory for new codebase
    -p <dir1[:d2:...]>  Use specified directories as the patch search path,
                         overriding the path= setting in the patchlist file,
			 and the default search path.
    -t <tarfile>        Use the specified tarfile as the source base,
                         overriding the base= setting in the patchlist file.
    -v, --verbose       Output extra information while running
    -V, --version       Display version information and quit

The patchlist file has one patch filename per line.  Lines starting with a #
are comments.  A line starting with "base=" specifies the base tarfile,
and a line starting with "path=" specifies the patch search path.
Relative paths are relative to the path where the patchfile resides.
""" % os.path.basename(progpath)
	sys.exit(0)


# unarchive_source unarchives the indicated tar file, into the directory
# specified.  Note that dest_dir replaces the top level
# directory name specified in the tar.
def unarchive_source(tarfile, dest_dir):
	global verbose

	print "Unarchiving %s to directory %s" % (tarfile, dest_dir)

	dest_dir = os.path.abspath(dest_dir)

	# make sure directory does not already exist.
	if os.path.exists(dest_dir):
		print "Cannot unarchive source file: %s" % tarfile
		print "Unarchive directory '%s' already exists." % dest_dir
		sys.exit(1)

	# determine what flags to use for decompressing and
	# extracting the tarfile
	if endswith(tarfile, ".bz2"):
		tarflags = "-xjf"
	elif endswith(tarfile, ".tar.gz") or endswith(tarfile, ".tgz"):
		tarflags = "-xzf"
	elif endswith(tarfile, ".tar"):
		tarflags = "-xf"
	else:
		print "Cannot unarchive source file: %s" % tarfile
		print "Don't know unarchive method for file with this extension."
		sys.exit(1)

	if verbose:
		verbose_flag = "-v"
	else:
		verbose_flag = ""

	# get directory to archive into
	parent_dir = os.path.dirname(dest_dir)
	unpack_dir = parent_dir+"/dopatch_tmp"
	# remove junk from previous run, if any
	if os.path.exists(unpack_dir):
		action = raw_input("Directory %s already exists, remove or cancel (r/c)? " % unpack_dir)
		if action=="r" or action=="R":
			action = raw_input("About to 'rm -rf %s', are you sure (y/n)? " % unpack_dir)
			if action=="y" or action=="Y":
				os.system("rm -rf %s" % unpack_dir)
			else:
				print "Remove not confirmed."
				sys.exit(0)
		else:
			print "Operation cancelled."
			sys.exit(0)
	os.mkdir(unpack_dir)
	
	command = "tar -C %s %s %s %s " % (unpack_dir, verbose_flag, tarflags, tarfile)
	ccode = os.system(command)
	if ccode:
		print "Error %s trying to unarchive source file: %s" % (ccode, tarfile)
		print "Cannot continue"
		sys.exit(ccode)

	# FIXTHIS - this code assumes that the archive has a single
	# top-level directory - the wildcard below is extremely dangerous

	# FIXTHIS - this code might act funky when parent_dir is 
	# a mount point

	# move the unpacked dir to the requested dir
	os.system("mv %s/* %s" % (unpack_dir, dest_dir))

	# remove unpack directory (dopatch_tmp)
	os.rmdir(unpack_dir)

def find_file(search_dirs, filename):
	for dir in search_dirs:
		file_path = os.path.abspath(dir+"/"+filename)
		if os.path.exists(file_path):
			return file_path
	raise ValueError, "Can't find file: %s" % filename

# apply_patches applies the list of patches to the files in
# the indicated directory
def apply_patches(source_dir, search_dirs, patch_list):
	global verbose

	# assume all patches use patch level 1 at root
	# FIXTHIS - this code assumes all patches use patch level one, 
	# at the root.

	# change to the parent of source_dir
	source_dir = os.path.abspath(source_dir)
	parent = os.path.basename(source_dir)
	curdir = os.getcwd()
	
	if verbose:
		silent_flag = ""
	else:
		silent_flag = "-s"

	for patch in patch_list:
		patch_file = find_file(search_dirs, patch)
		print "Applying patch: %s" % patch_file
		os.chdir(source_dir)
		ccode = os.system("patch %s --no-backup-if-mismatch -p1 <%s" % (silent_flag, patch_file))
		os.chdir(curdir)

def copy_tree(in_dir, extension):
	global verbose
	
	out_dir = in_dir + extension
	print "Copying to directory %s" % (out_dir)
	os.system("cp -a %s %s" % (in_dir, out_dir))

# read patch list from a file
# patches are individual file names, one per line
def read_patch_list(pl_file):
	global verbose

	# NOTES: no line can contain an equal sign
	# comments start with a #
	# whitespace at start of the line is ignored
	lines = open(os.path.abspath(pl_file)).readlines()
	patch_list = []
	pl_vars = {}
	for line in lines:
		# eat leading and trailing whitespace
		line = string.strip(line)

		# skip empty lines
		if len(line)==0:
			continue

		# skip comment lines
		if line[0]=="#":
			continue

		# process var assignments (base, path, ...?)
		if string.find(line,"=") != -1:
			(name,var) = string.split(line,"=",2)
			name = string.strip(name)
			var = string.strip(var)
			pl_vars[name] = var
			continue

		patch_list.append(line)
	return (patch_list, pl_vars)

def main():
	global verbose

	# set default values
	verbose = 0
	build_flag = 0
	copy_flag = 0
	tarfile = ""
	out_dir = ""
	patch_list = []
	pl_vars = {}
	pl_file = ""
	search_path_default = "."
	search_path = search_path_default

	# parse command line options
	if "-a" in sys.argv:
		arch = sys.argv[sys.argv.index("-a")+1]
	if "-b" in sys.argv:
		build_flag = 1
	if "-c" in sys.argv:
		copy_flag = 1
	if "-f" in sys.argv:
		pl_file = sys.argv[sys.argv.index("-f")+1]
	if "-h" in sys.argv or "--help" in sys.argv:
		usage(sys.argv[0])
	if "-l" in sys.argv:
		patch_list = sys.argv[sys.argv.index("-l")+1]
		patch_list = patch_list.split(",")
	if "-o" in sys.argv:
		out_dir = sys.argv[sys.argv.index("-o")+1]
	if "-p" in sys.argv:
		search_path = sys.argv[sys.argv.index("-p")+1]
	if "-t" in sys.argv:
		tarfile = sys.argv[sys.argv.index("-t")+1]
	if "-v" in sys.argv or "--verbose" in sys.argv:
		verbose = 1
	if "-V" in sys.argv or "--version" in sys.argv:
		show_version(sys.argv[0])
		sys.exit(0)

	# print program information, if verbose
	if verbose:
		show_version(sys.argv[0])

	if out_dir == "":
		out_dir = default_out_dir
		print "Using default output directory: %s" % out_dir
	if patch_list:
		print "Using specified patchlist: %s" % patch_list
	else:
		# get patchlist from file
		if pl_file == "":
			pl_file = default_patch_list_file
		if os.path.exists(pl_file):
			(patch_list, pl_vars) = read_patch_list(pl_file)
			print "Using patch list file:", os.path.abspath(pl_file)
			search_path_default = os.path.dirname(os.path.abspath(pl_file))	
			search_path = search_path_default
		else:
			print "Error: No patch list specified, aborting."
			sys.exit(1)

	# if no search path was specified on command line, and one
	# was specified in the patchlist file, use that
	if (search_path == search_path_default) and (pl_vars.has_key("path")):
		search_path = pl_vars["path"]

	# put search paths into a list
	# make relative paths relative to search_path_default
	search_dirs = []
	for dir in string.split(search_path,":"):
		if dir[0]!='/':
			dir = search_path_default+'/'+dir
		search_dirs.append(os.path.abspath(dir))

	print "search_dirs=", search_dirs

	# FIXTHIS - need to code unpack() - not done yet

	if tarfile == "":
		if pl_vars.has_key("base"):
			# base is relative to patch directory
			tarfile = find_file(search_dirs, pl_vars["base"])
			print "Using tarfile: %s" % tarfile
		else:
			tarfile = default_tarfile
			print "Using default tarfile: %s" % tarfile
	else:
		print "Using tarfile: %s" % tarfile
		
	unarchive_source(tarfile, out_dir)
	if patch_list:
		apply_patches(out_dir, search_dirs, patch_list)

	# FIXTHIS - need to code copy_to_orig() - not done yet
	if copy_flag:
		copy_tree(out_dir, ".orig")

	# FIXTHIS - need to code unpack() - not done yet
	if build_flag:
		copy_tree(out_dir, ".build")
		


if __name__=="__main__":
	main()
